use std::fmt;

use clap::Parser;
use clusters::DbDriverTag;
use common::types::{
    ConvexOrigin,
    ConvexSite,
};
use keybroker::{
    InstanceSecret,
    KeyBroker,
    DEV_INSTANCE_NAME,
    DEV_SECRET,
};
use metrics::SERVER_VERSION_STR;
use model::database_globals::types::StorageTagInitializer;
use serde_json::Value as JsonValue;
use url::Url;

#[derive(Parser, Clone)]
#[clap(version = &**SERVER_VERSION_STR, author = "Convex, Inc. <no-reply@convex.dev>", group(clap::ArgGroup::new("storage").multiple(false)))]
pub struct LocalConfig {
    /// File path for SQLite, the file path; for postgres, a server URL.
    #[clap(default_value = "convex_local_backend.sqlite3")]
    pub db_spec: String,

    /// Database driver type.
    #[clap(short, long, value_enum, default_value_t = DbDriverTag::Sqlite)]
    pub db: DbDriverTag,

    /// Host interface to bind to
    #[clap(short, long, default_value = "0.0.0.0")]
    pub interface: ::std::net::Ipv4Addr,

    /// Host port daemon should bind to
    #[clap(short, long, default_value = "3210")]
    pub port: u16,

    /// Host port to bind for Convex HTTP Actions
    #[clap(long, default_value = "3211")]
    site_proxy_port: u16,

    /// Origin of the Convex server, as accessible from the client.
    /// e.g. if the client is running on localhost, you can use the default of
    /// http://127.0.0.1:${port}.
    /// Otherwise, if the client is accessing the backend from the public
    /// internet, this would be the public URL of the backend, like
    /// https://api.my-app.com .
    /// Note the port in this url (usually 443, the https default) need not
    /// match the bind port `--port`.
    #[clap(long, requires = "convex_site")]
    convex_origin: Option<ConvexOrigin>,

    /// Origin of the Convex HTTP Actions, as accessible from the client.
    /// e.g. if the client is running on localhost, you can use the default of
    /// http://127.0.0.1:${site_proxy_port}.
    /// Otherwise, if the client is accessing the backend from the public
    /// internet, this would be the public URL of the backend, like
    /// https://my-app.com .
    /// Note the port in this url (usually 443, the https default) need not
    /// match the bind port `--site-proxy-port`.
    /// If you don't have a separate URL for HTTP Actions, you can use
    /// value from `--convex-origin` with a "/http" suffix, like
    /// https://api.my-app.com/http .
    #[clap(long, requires = "convex_origin")]
    convex_site: Option<ConvexSite>,

    /// Optional proxy for Actions fetches
    ///
    /// i.e. if doing `await fetch(request)` within an action, you can
    /// send the request through this proxy to screen it for SSRF attacks.
    #[clap(long)]
    pub convex_http_proxy: Option<Url>,

    /// Instance name for this backend.
    #[clap(long, requires = "instance_secret")]
    pub instance_name: Option<String>,

    /// Instance secret for this backend.
    #[clap(long, requires = "instance_name")]
    pub instance_secret: Option<String>,

    /// Identifier (like a user ID) to attach to any sentry
    /// events generated by this backend. Sentry is disabled
    /// by default.
    #[clap(long, hide = true)]
    pub sentry_identifier: Option<String>,

    /// Which directory should file storage use
    #[clap(long, group = "storage", default_value = "convex_local_storage")]
    local_storage: String,

    /// Use S3 storage instead of local storage.
    #[clap(long, group = "storage")]
    pub s3_storage: bool,

    /// If set, the persistence won't require SSL when talking to the database.
    /// It would still prefer SSL if available. This should only be set in
    /// tests.
    #[clap(long)]
    pub do_not_require_ssl: bool,

    /// self-hosted Convex will periodically communicate with a remote beacon
    /// server. This is to help Convex understand and improve the product.
    /// If set, the self-host beacon will not be sent.
    #[clap(long, env = "DISABLE_BEACON", value_parser = clap::builder::BoolishValueParser::new())]
    pub disable_beacon: bool,

    /// A tag to identify the self-hosted instance.
    #[clap(long, hide = true, default_value = "self-host")]
    pub beacon_tag: String,

    /// Extra fields to send to the beacon.
    #[clap(long, hide = true)]
    pub beacon_fields: Option<JsonValue>,

    /// If set, logs will be redacted from clients. Set this on production
    /// deployments, to prevent information like stacktraces of serverside
    /// code from being leaked to clients.
    ///
    /// On development deployments, it can be helpful to have this information
    /// reach the client for debugging purposes.
    #[clap(long, default_value = "false")]
    pub redact_logs_to_client: bool,

    /// Path of local file for logs to be routed to. For local testing
    /// of log integrations (eg axiom/datadog).
    #[clap(long)]
    pub local_log_sink: Option<String>,
}

impl fmt::Debug for LocalConfig {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Config")
            .field("convex_origin", &self.convex_origin)
            .field("convex_site", &self.convex_site)
            .field("instance_name", &self.instance_name)
            .finish()
    }
}

impl LocalConfig {
    pub fn http_bind_address(&self) -> ([u8; 4], u16) {
        (self.interface.octets(), self.port)
    }

    pub fn site_forward_prefix(&self) -> String {
        format!("http://127.0.0.1:{}/http", self.port)
    }

    pub fn site_bind_address(&self) -> Option<([u8; 4], u16)> {
        Some((self.interface.octets(), self.site_proxy_port))
    }

    pub fn convex_origin_url(&self) -> anyhow::Result<ConvexOrigin> {
        let origin = self
            .convex_origin
            .clone()
            .unwrap_or(format!("http://127.0.0.1:{}", self.port).into());
        // Allow empty origin so you can start up a self-hosted backend without
        // knowing its url yet.
        if !origin.is_empty() && !origin.starts_with("https://") && !origin.starts_with("http://") {
            anyhow::bail!(
                "Origin url should start with https:// or http:// but got '{}'",
                origin
            );
        }
        Ok(origin)
    }

    pub fn convex_site_url(&self) -> anyhow::Result<ConvexSite> {
        let site = self
            .convex_site
            .clone()
            .unwrap_or(format!("http://127.0.0.1:{}", self.site_proxy_port).into());
        // Allow empty site so you can start up a self-hosted backend without
        // knowing its url yet.
        if !site.is_empty() && !site.starts_with("https://") && !site.starts_with("http://") {
            anyhow::bail!(
                "Site url should start with https:// or http:// but got '{}'",
                site
            );
        }
        Ok(site)
    }

    pub fn name(&self) -> String {
        self.instance_name
            .clone()
            .unwrap_or(DEV_INSTANCE_NAME.to_owned())
    }

    pub fn key_broker(&self) -> anyhow::Result<KeyBroker> {
        let name = self.name();
        KeyBroker::new(&name, self.secret()?)
    }

    pub fn secret(&self) -> anyhow::Result<InstanceSecret> {
        InstanceSecret::try_from(
            self.instance_secret
                .clone()
                .unwrap_or(DEV_SECRET.to_owned())
                .as_str(),
        )
    }

    pub fn storage_tag_initializer(&self) -> StorageTagInitializer {
        if self.s3_storage {
            StorageTagInitializer::S3
        } else {
            StorageTagInitializer::Local {
                dir: self.local_storage.clone().into(),
            }
        }
    }

    #[cfg(test)]
    pub fn new_for_test() -> anyhow::Result<Self> {
        use anyhow::Context;

        let tempdir_handle = tempfile::tempdir()?;
        let db_path = tempdir_handle.path().join("convex_local_backend.sqlite3");
        // Easiest way to get a config object with defaults is to parse from cmd line
        let config = Self::try_parse_from([
            "convex-local-backend",
            db_path.to_str().context("invalid db path")?,
            "--local-storage",
            tempdir_handle
                .path()
                .to_str()
                .context("invalid local storage path")?,
        ])?;
        Ok(config)
    }
}
